{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Finetune Mistral 7B using NVIDIA NeMo and PEFT\n",
        "\n",
        "Welcome!\n",
        "\n",
        "In this notebook, we will use NVIDIA's [NeMo Framework](https://github.com/NVIDIA/NeMo) to finetune the Mistral 7B LLM. Finetuning the process of adjusting the weights of a pre-trained foundation model with custom data. Considering that foundation models can be significantly large, a variant of fine-tuning has gained traction recently, known as parameter-efficient fine-tuning (PEFT). PEFT encompasses several methods, including P-Tuning, LoRA, Adapters, and IA3.\n",
        "\n",
        "For those interested in a deeper understanding of these methods, we have included a list of additional resources at the end of this document.\n",
        "\n",
        "A note about running Jupyter Notebooks: Press Shift + Enter to run a cell. A * in the left-hand cell box means the cell is running. A number means it has completed. If your Notebook is acting weird, you can interrupt a too-long process by interrupting the kernel (Kernel tab -> Interrupt Kernel) or even restarting the kernel (Kernel tab -> Restart Kernel). Note restarting the kernel will require you to run everything from the beginning.\n",
        "\n",
        "# Deploy\n",
        "\n",
        "<p><a href=\"https://brev.nvidia.com/notebooks/mistralnemo?cuda=undefined&amp;python=undefined&amp;diskStorage=160&amp;file=https://github.com/brevdev/notebooks/blob/main/tensorrt_mistral.ipynb&amp;name=nemo-mistral&amp;instance=A100@a2-highgpu-1g:nvidia-tesla-a100:1&amp;baseImage=nvcr.io/nvidia/nemo:24.01.framework\"><img alt=\" Click here to deploy.\" src=\"https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg\"/></a></p>\n",
        "\n",
        "Click the badge above to quickly deploy this notebook inside of the NeMo container with no setup powered by Brev! Brev makes it easy to provision GPUs and deploy resources with a single click!"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### NeMo Tools and Resources:\n",
        "\n",
        " - [nvidia/GPT-2B-001 · Hugging Face](https://huggingface.co/nvidia/GPT-2B-001)\n",
        " - [NVIDIA/NeMo-Megatron-Launcher: NeMo Megatron launcher and tools (github.com)](https://github.com/NVIDIA/NeMo-Megatron-Launcher)\n",
        " - [NeMo/examples/nlp/language_modeling/tuning at main · NVIDIA/NeMo · GitHub](https://github.com/NVIDIA/NeMo/tree/main/examples/nlp/language_modeling/tuning)\n",
        "\n",
        " - [Understanding PEFT](https://lightning.ai/pages/community/article/understanding-llama-adapters/)\n",
        " - [NeMo Documentation](https://llm.ngc.nvidia.com/docs/model-customization-with-p-tuning.html#model-customization-with-p-tuning)\n",
        " - [NeMo LoRA Notebook](https://github.com/NVIDIA/NeMo/blob/stable/tutorials/nlp/lora.ipynb)\n",
        "\n",
        "\n",
        "### Requirements:\n",
        "\n",
        "### Software:\n",
        "\n",
        "-   NeMo Framework Container, version 23.05 or later\n",
        "-   Docker\n",
        "-   [NVIDIA AI Enterprise Product Support Matrix](https://docs.nvidia.com/ai-enterprise/latest/product-support-matrix/index.html#abstract)\n",
        "\n",
        "### Hardware:\n",
        "-   1X A100 GPU, preferably 80GB\n",
        "\n",
        "\"https://brev.nvidia.com/notebook/trtmistral1?cuda=undefined&amp;python=undefined&amp;diskStorage=256&amp;name=nvidia-tensorrt-mistral&amp;instance=A10G:g5.xlarge&amp;baseImage=nvcr.io/nvidia/tensorrt:24.01-py3\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Prepare the base model\n",
        "\n",
        "If you already have a .nemo file in your directory for the mistral models, you can skip this step.\n",
        "\n",
        "Otherwise, run the following cells to download the model and convert it to NeMo format"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "!pip install ipywidgets\n",
        "!jupyter nbextension enable --py widgetsnbextension"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "!mkdir -p models/mistral7b"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from huggingface_hub import notebook_login\n",
        "\n",
        "notebook_login()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "huggingface_hub.snapshot_download(repo_id=\"mistralai/Mistral-7B-v0.1\", local_dir=\"models/mistral7b\", local_dir_use_symlinks=False)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "!python /opt/NeMo/scripts/nlp_language_modeling/convert_hf_mistral_7b_to_nemo.py --in-file=models/mistral7b --out-file=models/mistral7b.nemo"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Prepare Data"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Next, we'll need to prepare the data that we're going to use for our LoRA fine tuning. Here we're going to be using the PubMedQA dataset, and we'll be training our model to respond with simple \"yes\" or \"no\" answers.\n",
        "\n",
        "First let's download the data and divide it into train/validation/test splits"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "!git clone https://github.com/pubmedqa/pubmedqa.git\n",
        "!cd pubmedqa/preprocess && python split_dataset.py pqal"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Now we can convert the PubMedQA data into the JSONL format that NeMo needs for Parameter Efficient Fine Tuning. We'll also reformat the data into prompts that our model can appropriately handle. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import json\n",
        "\n",
        "def write_jsonl(fname, json_objs):\n",
        "    with open(fname, 'wt') as f:\n",
        "        for o in json_objs:\n",
        "            f.write(json.dumps(o)+\"\\n\")\n",
        "\n",
        "def form_question(obj):\n",
        "    st = \"\"\n",
        "    st += f\"QUESTION:{obj['QUESTION']}\\n\"\n",
        "    st += \"CONTEXT: \"\n",
        "    for i, label in enumerate(obj['LABELS']):\n",
        "        st += f\"{obj['CONTEXTS'][i]}\\n\"\n",
        "    st += f\"TARGET: the answer to the question given the context is (yes|no|maybe): \"\n",
        "    return st\n",
        "\n",
        "def convert_to_jsonl(data_path, output_path):\n",
        "    data = json.load(open(data_path, 'rt'))\n",
        "    json_objs = []\n",
        "    for k in data.keys():\n",
        "        obj = data[k]\n",
        "        prompt = form_question(obj)\n",
        "        completion = obj['reasoning_required_pred']\n",
        "        json_objs.append({\"input\": prompt, \"output\": completion})\n",
        "    write_jsonl(output_path, json_objs)\n",
        "    return json_objs\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "test_json_objs = convert_to_jsonl(\"pubmedqa/data/test_set.json\", \"pubmedqa_test.jsonl\")\n",
        "train_json_objs = convert_to_jsonl(\"pubmedqa/data/pqal_fold0/train_set.json\", \"pubmedqa_train.jsonl\")\n",
        "dev_json_objs = convert_to_jsonl(\"pubmedqa/data/pqal_fold0/dev_set.json\", \"pubmedqa_val.jsonl\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Here's an example of what the data looks like"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "test_json_objs[0]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Run Training"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "NeMo Framework uses config objects to control many of its operations, which allows you to quickly see what options you can change and carry out different experiments. We can start by downloading an example config file from github."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "!wget https://raw.githubusercontent.com/NVIDIA/NeMo/main/examples/nlp/language_modeling/tuning/conf/megatron_gpt_finetuning_config.yaml"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Now we'll read in this default config file with Hydra, and apply an override that enables the use of Megatron core. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import hydra\n",
        "from omegaconf.omegaconf import OmegaConf\n",
        "\n",
        "hydra.initialize(version_base=None, config_path=\".\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "cfg = hydra.compose(config_name=\"megatron_gpt_finetuning_config\", overrides=['++model.mcore_gpt=True'])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "To see all of the different configuration options available, you can take a look at the file we downloaded. For this example, we're going to update a couple of settings to point to our datasets and run LoRA tuning on our A100. Feel free to experiment with these different options!\n",
        "\n",
        "For data our data configuration, we'll point to the JSONL files we wrote out earlier. `concat_sampling_probabilities` determines what percentage of the finetuning data you would like to come from each file -- in our example we only have 1 train file so we choose `[1.0]`"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "OmegaConf.update(cfg, \"model.data\", {\n",
        "  \"train_ds\": {\n",
        "      \"num_workers\": 0,\n",
        "      \"file_names\": [\"pubmedqa_train.jsonl\"],\n",
        "      \"concat_sampling_probabilities\": [1.0]\n",
        "  },\n",
        "  \"validation_ds\": {\n",
        "      \"num_workers\": 0,\n",
        "      \"file_names\": [\"pubmedqa_val.jsonl\"]\n",
        "  },\n",
        "  \"test_ds\": {\n",
        "    \"file_names\": [\"pubmedqa_test.jsonl\"],\n",
        "    \"names\": [\"pubmedqa\"]\n",
        "  }\n",
        "}, merge=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "For our model settings, we don't have much to change since we're reading in a pretrained model. We need to point to our existing/converted `.nemo` file, specify that we want to use LoRA as our scheme for finetuning, and choose our parallelism and batch size values. The values below should be appropriate for a single A100 GPU."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "OmegaConf.update(cfg, \"model\", {\n",
        "    \"restore_from_path\": \"models/mistral7b.nemo\",\n",
        "    \"peft\": {\n",
        "        \"peft_scheme\": \"lora\"\n",
        "    },\n",
        "    \"tensor_model_parallel_size\": 1,\n",
        "    \"pipeline_model_parallel_size\": 1,\n",
        "    \"micro_batch_size\": 1,\n",
        "    \"global_batch_size\": 8,\n",
        "}, merge=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Finally, we set some training specific options. We're training on 1 GPU on a single node at bfloat16 precision. For this example we'll also only train for 50 steps."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "OmegaConf.update(cfg, \"trainer\", {\n",
        "    'devices': 1,\n",
        "    'num_nodes': 1,\n",
        "    'precision': \"bf16-mixed\",\n",
        "    \"val_check_interval\": 10,\n",
        "    \"max_steps\": 20\n",
        "})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "With our configurations set, we are ready to initialize our `Trainer` object to handle our training loop, and an experiment manager to handle checkpointing and logging. After initializing the Trainer object we can load our model from disk into memory. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "from nemo.collections.nlp.models.language_modeling.megatron_gpt_sft_model import MegatronGPTSFTModel\n",
        "from nemo.collections.nlp.parts.megatron_trainer_builder import MegatronLMPPTrainerBuilder\n",
        "from nemo.collections.nlp.parts.peft_config import LoraPEFTConfig\n",
        "from nemo.utils.exp_manager import exp_manager\n",
        "\n",
        "trainer = MegatronLMPPTrainerBuilder(cfg).create_trainer()\n",
        "exp_manager(trainer, cfg.exp_manager)\n",
        "\n",
        "model_cfg = MegatronGPTSFTModel.merge_cfg_with(cfg.model.restore_from_path, cfg)\n",
        "model = MegatronGPTSFTModel.restore_from(cfg.model.restore_from_path, model_cfg, trainer=trainer)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Before training our adapter, let's see how the base model performs on the dataset"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "trainer.test(model)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Now, let's add the LoRA Adapter and train it:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "model.add_adapter(LoraPEFTConfig(model_cfg))\n",
        "trainer.fit(model)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Finally, we can see how the newly finetuned model performs on the test data:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "trainer.test(model)"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.10.12"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}
